package jmine.tec.report.impl.table.pdf;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import jmine.tec.report.api.RendererTarget;
import jmine.tec.report.api.ReportElement;
import jmine.tec.report.api.table.ReportCell;
import jmine.tec.report.api.table.ReportColumn;
import jmine.tec.report.api.table.ReportRow;
import jmine.tec.report.api.table.ReportTable;
import jmine.tec.report.api.table.ReportTableGroup;
import jmine.tec.report.impl.table.RendererParameterInterface;
import jmine.tec.report.pdf.ReportElementPDFRenderer;

import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfPTable;

/**
 * Renderer padrão de tabelas para relatórios
 * 
 * @author lundberg
 */
public class ReportTablePDFRenderer implements ReportElementPDFRenderer {

    // Magic numbers
    private static final float TABLE_DEFAULT_WIDTH = 100f;

    private static final float GRAY_FILL = 0.95f;

    private CellRendererPDFRegistry cellRendererRegistry;

    /**
     * {@inheritDoc}
     */
    public String getRendererType() {
        return ReportTable.RENDERER;
    }

    /**
     * {@inheritDoc}
     */
    public void render(Document document, ReportElement element) throws IOException {
        this.doRender(document, (ReportTable<?>) element);
    }

    /**
     * Modifica o documento pdf para adicionar o elemento
     * 
     * @param document document
     * @param element element
     * @throws IOException IOException
     */
    private void doRender(Document document, ReportTable<?> element) throws IOException {
        try {
            this.generateTableHeader(document, element);
            this.generatePDFTableModel(document, element);
        } catch (DocumentException e) {
            throw new IOException("Erro gerando documento PDF! " + e.getMessage());
        }
    }

    /**
     * Cria o cabeçalho da tabela (em geral contém o título da tabela, eventualmente alguma outra informação relevante)
     * 
     * @param document document
     * @param element element
     * @throws DocumentException DocumentException
     */
    private void generateTableHeader(Document document, ReportTable<?> element) throws DocumentException {
        List<ReportCell> tableHeader = element.getTableHeader();
        if (!tableHeader.isEmpty()) {
            PdfPTable pdfTable = new PdfPTable(tableHeader.size());
            pdfTable.setWidthPercentage(TABLE_DEFAULT_WIDTH);
            pdfTable.setTotalWidth(document.getPageSize().width());
            pdfTable.getDefaultCell().setBorder(Rectangle.NO_BORDER);
            for (ReportCell cell : tableHeader) {
                this.renderCell(pdfTable, cell);
            }
            document.add(pdfTable);
            document.add(Chunk.NEWLINE);
        }
    }

    /**
     * Popula um {@link com.lowagie.text.pdf.PdfTable} com os dados da tabela passada.
     * 
     * @param document document
     * @param element element
     * @param <V> tipo de valor
     * @throws DocumentException DocumentException
     */
    private <V> void generatePDFTableModel(Document document, ReportTable<V> element) throws DocumentException {
        /* creating the PDF Table */
        PdfPTable pdfTable = new PdfPTable(this.countRenderedColumns(element));
        pdfTable.setWidthPercentage(TABLE_DEFAULT_WIDTH);
        pdfTable.setTotalWidth(document.getPageSize().width());

        this.defineWidths(pdfTable, element);

        this.generateTableContentHeader(pdfTable, element);

        this.generateTableContent(pdfTable, element);

        this.generateTableFooter(pdfTable, element);

        document.add(pdfTable);
        document.add(Chunk.NEWLINE);
    }

    /**
     * Conta o número de colunas que devem ser renderizadas
     * 
     * @param <V> tipo de registro
     * @param element tabela
     * @return integer
     */
    private <V> int countRenderedColumns(ReportTable<V> element) {
        int rendered = 0;
        for (ReportColumn<V> column : element.getColumns()) {
            if (!this.createRPI(column).isHidden()) {
                rendered++;
            }
        }
        return rendered;
    }

    /**
     * Popula os header de um {@link com.lowagie.text.pdf.PdfTable}.
     * 
     * @param pdfTable tabela do PDF aonde os dados devem ser gravados
     * @param element element
     * @param <V> tipo de valor
     */
    private <V> void generateTableContentHeader(PdfPTable pdfTable, ReportTable<V> element) {
        pdfTable.setHeaderRows(1);
        pdfTable.getDefaultCell().setBorder(Rectangle.BOTTOM);
        for (ReportColumn<V> column : element.getColumns()) {
            if (!this.createRPI(column).isHidden()) {
                ReportCell cell = column.getHeader();
                if (cell != null) {
                    this.renderCell(pdfTable, cell);
                } else {
                    pdfTable.addCell("");
                }
            }
        }
    }

    /**
     * Popula os dados de um {@link com.lowagie.text.pdf.PdfTable} com o conteudo da tabela original.
     * 
     * @param pdfTable tabela do PDF aonde os dados devem ser gravados
     * @param element element
     * @param <V> tipo de valor da coluna
     */
    @SuppressWarnings("unchecked")
    private <V> void generateTableContent(PdfPTable pdfTable, ReportTable<V> element) {
        pdfTable.getDefaultCell().setBorder(Rectangle.NO_BORDER);
        pdfTable.getDefaultCell().setNoWrap(false);

        Iterator<ReportRow<V>> rowIterator = element.createRowIterator();

        int index = 0;
        /* fill the table with the data. */
        while (rowIterator.hasNext()) {
            ReportRow<V> row = rowIterator.next();
            if (index % 2 == 1) {
                pdfTable.getDefaultCell().setGrayFill(GRAY_FILL);
            } else {
                pdfTable.getDefaultCell().setGrayFill(1);
            }
            index++;
            for (ReportColumn<V> column : element.getColumns()) {
                if (!this.createRPI(column).isHidden()) {
                    ReportCell cell = row.getCellMap().get(column.getId());
                    this.renderCell(pdfTable, cell);
                }
            }
        }

        if (index == 0 && !element.getTableGroup().isEmpty()) {
            for (ReportTableGroup reportTableGroup : element.getTableGroup()) {
                List<ReportRow<V>> rows = reportTableGroup.onGroupEnd();
                for (ReportRow<V> row : rows) {
                    for (ReportColumn<V> column : element.getColumns()) {
                        if (!this.createRPI(column).isHidden()) {
                            ReportCell cell = row.getCellMap().get(column.getId());
                            this.renderCell(pdfTable, cell);
                        }
                    }
                }

            }
        }

        pdfTable.getDefaultCell().setGrayFill(1);
    }

    /**
     * Popula o footer da tabela
     * 
     * @param pdfTable tabela do PDF aonde os dados devem ser gravados
     * @param element element
     * @param <V> tipo de valor
     */
    private <V> void generateTableFooter(PdfPTable pdfTable, ReportTable<V> element) {
        pdfTable.getDefaultCell().setHorizontalAlignment(Element.ALIGN_LEFT);
        pdfTable.getDefaultCell().setBorder(Rectangle.TOP);
        pdfTable.getDefaultCell().setHorizontalAlignment(Element.ALIGN_RIGHT);
        for (ReportColumn<V> column : element.getColumns()) {
            if (!this.createRPI(column).isHidden()) {
                ReportCell cell = column.getFooter();
                if (cell != null) {
                    this.renderCell(pdfTable, cell);
                } else {
                    pdfTable.addCell("");
                }
            }
        }
    }

    /**
     * Define a largura das colunas
     * 
     * @param <V> tipo de valor
     * @param pdfTable tabela onde o elemento foi escrito, que deve ter as larguras definidas
     * @param element element
     */
    private <V> void defineWidths(PdfPTable pdfTable, ReportTable<V> element) {
        float[] widths = new float[this.countRenderedColumns(element)];
        int i = 0;
        for (ReportColumn<V> column : element.getColumns()) {
            if (!this.createRPI(column).isHidden()) {
                widths[i++] = this.createRPI(column).getRelativeWidth().floatValue();
            }
        }
        try {
            pdfTable.setWidths(widths);
        } catch (DocumentException e) {
            // se nao for possivel atualizar a largura das colunas, ignorar o
            // erro
        }
    }

    /**
     * Cria uma célula na tabela
     * 
     * @param pdfTable pdfTable
     * @param cell cell
     */
    private void renderCell(PdfPTable pdfTable, ReportCell cell) {
        ReportCellPDFRenderer renderer = this.cellRendererRegistry.getRenderer(cell.getRendererType());
        renderer.createCell(pdfTable, cell);
    }

    /**
     * @return the cellRendererRegistry
     */
    public CellRendererPDFRegistry getCellRendererRegistry() {
        return this.cellRendererRegistry;
    }

    /**
     * @param cellRendererRegistry the cellRendererRegistry to set
     */
    public void setCellRendererRegistry(CellRendererPDFRegistry cellRendererRegistry) {
        this.cellRendererRegistry = cellRendererRegistry;
    }

    /**
     * Cria um RendererParameterInterface para o target
     * 
     * @param target target
     * @return RendererParameterInterface
     */
    private RendererParameterInterface createRPI(RendererTarget target) {
        return new RendererParameterInterface(target);
    }
}
